#####################################################################
# Product:      KnowledgeEngine (Plone Edition)
#
# File:         Assessment.py
#
# Copyright:    Copyright (c) 2005, Faulkner Technologies
#
# Author:       Brian Oliver
#
# Description:  An Assessment is storage-agnostic in-memory 
#               representation of the state of an assessment, 
#               information of which is typically captured during 
#               the process of 'performing an assessment' against a Model.
#
#               This implementation is lazy, in that it delays the
#               actual retrieving of assessment information from
#               an AssessmentStorageService until required.
#
#               To create, retrieve, update or delete an 
#               Assessment, you should use the AssessmentStorageService.
#
#               NOTE 1: Instances of this class should ALWAYS be pickleable
#               as to permit storage of an assessment by python pickling.
#               An example usage: Temporarily storing an assessment in
#               a Zope HTTP Session
#
#               NOTE 2: This class replaces the 0.x branch class
#               AssessmentCache.  The difference being that this
#               implementation is storage independant where as 
#               AssessmentCache was tightly coupled with the MySQL
#               implementation.
#
#               NOTE 3: This class additionally supports the notion
#               of 'Assessment Attributes' that may be used to persist
#               state that is not directly related to individual component
#               state.  For example: The score for an assessment may
#               be 'cached' as an assessment attribute to avoid expensive
#               recalculation.  The principle concept of assessment
#               attributes is that they may be dropped at any time, 
#               without effecting the semantics of the actual 
#               assessment state captured while 'performing an assessment'
#
# See Also:     Assessible, AssessmentComponentValueTypes,
#               AssessmentComponentState, AssessmentAttribute,
#               AssessmentStorageService
#####################################################################

#####################################################################
## KnowledgeEngine Library Imports
from Common import *
from Core import *

#####################################################################
## Zope Library Imports
from Globals import InitializeClass
from AccessControl import ClassSecurityInfo
from AccessControl.Role import RoleManager
from DateTime import DateTime
 
#####################################################################
## Python Library Imports
import os
import time

#####################################################################
## Python Logging Configuration
import logging
log = logging.getLogger("KnowledgeEngine.Assessment")

#####################################################################
## Assessment Class Definition
class Assessment(RoleManager):
    """
        A lazily in-memory storage-agnostic representation of an
        Assessment.
    """
        
    #---------------------------------------------------------------#

    #declare security so ClassInfo may be accessed in DTML/ZPT etc
    security = ClassSecurityInfo()
    security.declareObjectPublic()
    security.setDefaultAccess("allow")

    #---------------------------------------------------------------#

    def __init__(self, assessmentIdentity, model, openedByUserIdentity = None, openedFromRepositoryIdentity = None):
        """
            Assessments are constructed using using four properties;
            * The identity of the assessment
            * The model to which the assessment pertains
        """
        
        ### the following attributes are generally not persisted - they are transient        
        #a dictionary of AssessmentComponentState instances, for Assessible components indexed by ComponentIdentity.
        #{Component.getIdentity -> AssessmentComponentState}
        self.componentState = {}
        
        #a dictionary of AssessmentAttribute instances, indexed by AssessmentAttributeHandle.getAttributeIdentity()
        #{AssessmentAttributeHandle.getAttributeIdentity() -> AssessmentAttribute}
        self.attributes = {}        
        self.openedBy = openedByUserIdentity
        self.openedFromRepositoryIdentity = openedFromRepositoryIdentity
        
        ### the following attributes are generally stored together (and need to be persisted)
        self.identity = assessmentIdentity
        self.model = model                          #only the model.getIdentity() is actually persisted
        self.instantCreated = DateTime()            
        self.instantModified = DateTime()          
        self.createdBy = 'anonymous'                #who created the assessment 
        self.isLocked = false                       #once locked an assessment may not be changed
        self.name = '(new assessment)'              #the user-friendly/provided name for the assessment
        self.checkedOutBy = None                    #the user identity that has the assessment checkedout (or None)
        self.creditTransactionIdentity = None       #the credit transaction used to create the assessment (or None)
        
        self.isNew = true
        self.isModified = true

    #---------------------------------------------------------------#

    def getIdentity(self):
        """
            Returns the unique identity for the assessment
            
            RETURN TYPE: String
        """
        
        return self.identity

    #---------------------------------------------------------------#

    def getModel(self):
        """
            Returns the Model for the assessment.
            
            RETURN TYPE: Model Class            
        """
        
        return self.model

    #---------------------------------------------------------------#

    def getModelIdentity(self):
        """
            Returns the Model identity for the assessment.
            
            RETURN TYPE: String
        """
        
        return self.model.getIdentity()

    #---------------------------------------------------------------#

    def getOpenedBy(self):
        """
            Returns the Knowledge Engine User Identity that opened the 
            assessment.  May return None if the system (or someone unknown)
            opened the assessment.
            
            NOTE: Not persistent

            RETURN TYPE: String (UserIdentity)
        """
        
        return self.openedBy

    #---------------------------------------------------------------#

    def getOpenedFromRepositoryIdentity(self):
        """
            Returns the identity of the repository from which 
            the assessment was opened/created.
            
            May return None if the opening repository is unknown.
            
            NOTE: Not persistent

            RETURN TYPE: String
        """
        
        return self.openedFromRepositoryIdentity

    #---------------------------------------------------------------#

    def getStorageService(self):
        """
            RETURNS: The AssessmentStorageService that may be used
            to persist the assessment
        """

        #lookup the assessment storage service        
        return self.getModel().getAssessmentStorageService()

    #---------------------------------------------------------------#

    def getInstantCreated(self):
        """
            Returns the instant in time (as a DateTime) that the assessment was created
            
            RETURN TYPE: DateTime   
        """

        return self.instantCreated

    #---------------------------------------------------------------#

    def setInstantCreated(self, instantCreated):
        """
            Sets the instant in time (as a DateTime) that the assessment was created
        """

        self.instantCreated = instantCreated

    #---------------------------------------------------------------#

    def getInstantModified(self):
        """
            Returns the instant in time (as a DateTime) that the assessment was modified
            
            RETURN TYPE: DateTime
        """

        return self.instantModified

    #---------------------------------------------------------------#

    def setInstantModified(self, instantModified):
        """
            Sets the instant in time (since the EPOCH) that the assessment was modified
        """

        self.instantModified = instantModified
        
    #---------------------------------------------------------------#

    def getCreatedBy(self):
        """
            Returns the User Identity that created the assessment
        
            RETURN TYPE: String
        """
        
        return self.createdBy

    #---------------------------------------------------------------#

    def setCreatedBy(self, userIdentity):
        """
            Sets the user identity that created the assessment
        """
        
        self.createdBy = userIdentity
        self.isModified = true
        
    #---------------------------------------------------------------#

    def getCheckedOutBy(self):
        """
            Returns the User Identity that has the assessment checkedout
            (None if not checkedout)
        
            RETURN TYPE: String
        """
        
        return self.checkedOutBy

    #---------------------------------------------------------------#

    def setCheckedOutBy(self, userIdentity):
        """
            Sets the user identity that has the assessment checkedout
        """
        
        if userIdentity == "":
            self.checkedOutBy = None
        else:
            self.checkedOutBy = userIdentity 
            
        self.isModified = true
        
    #---------------------------------------------------------------#

    def isCheckedOut(self):
        """
            Returns if this assessment is currently checkedout.
            
            This includes logic to determine 
        """
        
        #we only need to check if the model doesn't allow concurrent assessment
        if not self.getModel().getPermitsConcurrentAssessment():

            #does the assessment think it's checked out?
            if self.getCheckedOutBy() is not None and self.getCheckedOutBy() != "":

                #has enough time passed since the last modification to warrant that the assessment may not be checkedout?
                if self.getInstantModified().lessThan(DateTime().timeTime() - (getMaximumAssessmentCheckoutTime() * 60)):
                    #outside the time period now... so assume not checked out
                    return false
                else:
                    #still within the timeout period
                    return true                

            #not checked out
            else:
                return false            
        
        #doing concurrent assessment so multiple users permitted
        else:
            return false
        
    #---------------------------------------------------------------#

    def getCreditTransactionIdentity(self):
        """
            Returns the Credit Transaction Identity that was used
            to create the assessment (may return '' or None).
            
            This is used to track/audit the consumption of credits
            when creating assessment.  It's also used when refunding
            credits after deleting assessments created using credits.
        
            RETURN TYPE: String
        """
        
        return self.creditTransactionIdentity

    #---------------------------------------------------------------#

    def setCreditTransactionIdentity(self, creditTransactionIdentity):
        """
            Sets the Credit Transaction Identity that was used
            to create the assessment (may return '' or None).
        """
        
        self.creditTransactionIdentity = creditTransactionIdentity
        self.isModified = true

    #---------------------------------------------------------------#

    def getIsLocked(self):
        """
            Returns if the assessment is locked
            
            RETURN TYPE: Boolean
        """

        return self.isLocked

    #---------------------------------------------------------------#

    def setIsLocked(self, isLocked):
        """
            Sets if the assessment is locked
            
            NOTE: If you lock or unlock an assessment, you MUST
            ensure the Model.onBeforeLock/UnlockAssessment is called
            prior to saving the assessment to ensure correct
            events are handled.
        """

        self.isLocked = isLocked
        self.isModified = true

    #---------------------------------------------------------------#

    def getName(self):
        """
            Returns the user provided name for the assessment
        
            RETURN TYPE: String
        """
        
        #if the name is empty, return "(unnamed)"
        if self.name is None or self.name == "":
            return "(unnamed)"
        else:            
            return self.name

    #---------------------------------------------------------------#

    def setName(self, name):
        """
            Sets the user provided name for the assessment
        """
        
        self.name = name
        self.isModified = true

    #---------------------------------------------------------------#

    def getIsNew(self):
        """
            Returns if the assessment state is new (not yet persisted)
            
            RETURN TYPE: Boolean
        """

        return self.isNew

    #---------------------------------------------------------------#

    def setIsNew(self, isNew):
        """
            Sets if the assessment state is new (not yet persisted)
        """

        self.isNew = isNew

    #---------------------------------------------------------------#

    def getIsModified(self):
        """
            Returns if the assessment state has been modified since created or loaded 
            
            RETURN TYPE: Boolean
        """

        if self.isModified:
            return true
        else:
            #check the component state to determine if any of them are modified
            for state in self.getComponentStates():
                #is the current component modified?
                if state.getIsModified():
                
                    #as the current component is modified we set that the actual assessment is modified
                    self.setIsModified(true)

            return self.isModified
            
    #---------------------------------------------------------------#

    def setIsModified(self, isModified):
        """
            Sets if the assessment state has been modified since created or loaded 
        """

        self.isModified = isModified
            
    #---------------------------------------------------------------#

    def setComponentState(self, componentIdentity, componentState):
        """
            Sets the state of the specified component 
            (an instance of AssessmentComponentState).  
        """

        #set the component state
        self.componentState[componentIdentity] = componentState

        #the cache has been modified
        self.isModified = true
            
    #---------------------------------------------------------------#

    def getComponentState(self, componentIdentity):
        """
            Retrieves the state of the specified component.
            (an instance of the AssessmentComponentState).
            
            If the assessment does not have state for the component,
            The 'default state' for the component is added to the 
            assesment.
            
            RETURN TYPE: AssessmentComponentState
        """

        starttime = time.clock()
        
        #is the state in the component?
        if self.componentState.has_key(componentIdentity):
        
            #get the state from the internal cache
            assessmentComponentState = self.componentState[componentIdentity]
        
        else:
            #attempt to get the state from the AssessmentStorageService
            storageService = self.getStorageService()
            
            #if we can, load the state from the storage tool?
            assessmentComponentState = None
            if storageService is not None:
                #(load the state using the storageService)
                assessmentComponentState = storageService.loadComponentState(self, componentIdentity)
                
            #did we load it?
            if assessmentComponentState is None:
                #(use the default assessment state from the component itself)
                #get the component from the model
                component = self.getModel().getComponent(componentIdentity)
                            
                # FIXME: Define exceptions properly
                if not component:
                    raise Exception, "Could not find component with identity " + `componentIdentity`

                #get the default state of the component from the component itself (using this assessment)
                assessmentComponentState = component.getDefaultAssessmentState(self)
            
            #add the state to the assessment
            self.componentState[componentIdentity] = assessmentComponentState

        endtime = time.clock()

        log.debug("%s:(%d): %s" % (sys._getframe().f_code.co_name, sys._getframe().f_lineno, "Loaded Component State in %f" % (endtime-starttime)))        

        return assessmentComponentState         

    #---------------------------------------------------------------#

    def getComponentStates(self):
        """
            Retrieves a list of all of the currently cached component states
            
            NOTE: does not forcibly load all of the component states
            for the model (this implementation is lazy)
        """
            
        return self.componentState.values()

    #---------------------------------------------------------------#

    def getAttributes(self):
        """
            Retrieves a list of all of the currently cached attributes
            
            NOTE: does not forcibly load all of the component states
            for the model (this implementation is lazy)
        """
            
        return self.attributes.values()
            
    #---------------------------------------------------------------#

    def setAttribute(self, assessmentAttribute):
        """
            Sets the state of the specified attribute 
            (an instance of AssessmentAttribute).  
        """

        #set the component state
        self.attributes[assessmentAttribute.getAttributeHandle().getAttributeIdentity()] = assessmentAttribute

        #the cache has been modified
        self.isModified = true
            
    #---------------------------------------------------------------#

    def getAttribute(self, attributeHandle):
        """
            Retrieves the state of the specified attribute.
            (an instance of the AssessmentAttribute).
            
            If the assessment does not have the specified attribute,
            None is returned.
            
            RETURN TYPE: AssessmentAttribute
        """
        
        #is the attribute in the attributes collection?
        if self.attributes.has_key(attributeHandle.getAttributeIdentity()):
        
            #get the attribute from the internal cache
            assessmentAttribute = self.attributes[attributeHandle.getAttributeIdentity()]
        
        else:
            #attempt to get the attribute from the AssessmentStorageService
            storageService = self.getStorageService()
            
            #if we can, load the attribute from the storage service?
            assessmentAttribute = None
            if storageService is not None:
                #(load the attribute using the storageService)
                assessmentAttribute = storageService.loadAttribute(self, attributeHandle)
            
            #add the attribute to the internal cache (if it's not None!)
            if assessmentAttribute is not None:
                self.attributes[attributeHandle.getAttributeIdentity()] = assessmentAttribute

        return assessmentAttribute         
               
    #---------------------------------------------------------------#
        
#####################################################################
## Class Initialisation
InitializeClass(Assessment)
